using PyCall
using Conda
using Graphs
using WGLMakie
using CairoMakie
using GraphMakie
using GraphMakie.NetworkLayout
using JSON3
using JSServe, Markdown
Page(exportable=true, offline=true) # for Franklin, you still need to configure
WGLMakie.activate!()
Makie.inline!(true) # Make sure to inline plots into Documenter output!
nothingGraphing CGP Grey’s Rock-Paper-Scissors YouTube Game
python, julia, cgpgrey rock paper scissors youtube game
If you haven’t already checked it out, go watch CGPGrey’s Rock-Paper-Scissors YouTube Game.
In this post, I’m going to explore what all the possible paths available are. Let’s import some packages first.
Fortunately for us, CGPGrey was kind enough to put links to the choices in the description of (almost) every video. We can use Google’s YouTube API to get the video descriptions and get all the YouTube links in the description.
We are going to use the google-api-python-client in Python from Julia.
Code
API_KEY = ENV["YOUTUBE_API_KEY"]; # Get API_KEY from google console
build = pyimport("googleapiclient.discovery").build # from googleapiclient.discovery import build
youtube = build("youtube", "v3", developerKey=API_KEY) # call build function in PythonPyObject <googleapiclient.discovery.Resource object at 0x16457f340>
Now we can get the description of every video, extract the metadata from it into a Dict of Dicts, build a graph:
Code
youtubeid(url) = string(first(split(replace(url, "https://www.youtube.com/watch?v="=>""), "&t")))
function metadata(url)
id = youtubeid(url)
request = youtube.videos().list(part="snippet", id=id)
response = request.execute()
description = response["items"][1]["snippet"]["description"]
title = response["items"][1]["snippet"]["title"]
if isempty(description)
# Special case for https://www.youtube.com/watch?v=CPb168NUwGc (description for this is empty)
return (; description = """
WIN: https://www.youtube.com/watch?v=RVLUX6BUEJI
LOSE / DRAW: https://www.youtube.com/watch?v=jDQqv3zkbIQ
🌐 Website: https://www.cgpgrey.com
💖 Patreon: https://www.patreon.com/cgpgrey
📒 Cortex: http://www.cortexbrand.com
⛔️ Ah01F ✅
""", title = "🔴")
end
(; description, title)
end
function links(url; visited=Dict(), duplicate_links = false)
m = metadata(url)
r = Dict(
:id => youtubeid(url),
:code => last(split(strip(m.description), "\n")), # last line is a special code
:url => url,
:links => [],
:children => [],
:title => m.title
)
for line in split(m.description, "\n")
if occursin("https://www.youtube.com/watch?v=", line)
_status, video = split(line, ":", limit=2)
video = strip(video)
push!(r[:links], Dict(:status => string(_status), :url => string(video)))
end
end
for link in r[:links]
url = link[:url]
if !(url in keys(visited))
visited[url] = Dict()
s = links(url; visited, duplicate_links)
push!(r[:children], s)
visited[url] = s
else
duplicate_links && push!(r[:children], visited[url])
end
end
return r
end
function cached_links(url; duplicate_links)
bfile = """$(youtubeid(url))-$(duplicate_links ? "dup-links" : "no-dup-links").json"""
if isfile(bfile)
return JSON3.read(bfile)
end
r = links(url; duplicate_links)
open(bfile, "w") do f
JSON3.write(f, r)
end
r
end
function _clean_titles(str)
t = join([c for c in str if isascii(c)])
t = strip(t)
if occursin("Cortex", t)
return ""
end
string(t)
end
function _node_builder(nodes, d)
for c in d[:children]
push!(nodes, (; id = c[:id], title = _clean_titles(c[:code]), url = c[:url]))
_node_builder(nodes, c)
end
end
function _graph_builder(G, d, ids)
from = d[:id]
for c in d[:children]
to = c[:id]
add_edge!(G, findfirst(isequal(from), ids), findfirst(isequal(to), ids))
_graph_builder(G, c, ids)
end
end
function get_nodes(data)
nodes = [(; id = data[:id], title = _clean_titles(data[:title]), url = data[:url])]
_node_builder(nodes, data)
nodes = unique(nodes)
ids = [n.id for n in nodes]
titles = [n.title for n in nodes]
urls = [n.url for n in nodes]
(; ids, titles, urls, nodes)
end
function grapher(data, ids)
G = SimpleDiGraph(length(ids))
_graph_builder(G, data, ids)
G
enddata = cached_links("https://www.youtube.com/watch?v=PmWQmZXYd74", duplicate_links = true)
(; ids, titles, urls, nodes) = get_nodes(data)
G = grapher(data, ids){111, 206} directed simple Int64 graph
There’s 111 videos in the Graph with 206 connections between the videos.
Here’s what that graph visualized looks like:
Code
WGLMakie.activate!()
set_theme!(; resolution=(1600, 900))
empty_theme = Theme(
fonts=(; title = "CMU Serif"),
)
# If there's a space it is probably a unique name
node_colors = [length(split(strip(t))) > 1 ? :red : :black for t in titles]
node_size = [length(split(strip(t))) > 1 ? 8 : 5 for t in titles]
f, ax, p = graphplot(G;
nlabels=titles,
nlabels_fontsize=10,
node_color=node_colors,
node_size,
arrow_size=8,
layout=Stress(dim=3)
)
# hidedecorations!(ax); hidespines!(ax);
# offsets = [Point2f(0.1, -0.5) for _ in p[:node_pos][]]
# offsets[1] = Point2f(0.1, 0.5)
# p.nlabels_offset[] = offsets
# autolimits!(ax)
# ax.title = "CGP Grey's Rock-Paper-Scissors YouTube Game"This graph contains a lot of duplicate links to the same video. For example when losing after different number of wins, you might end up at the same video. Let’s remove those connections so we can visualize it as a tree.
Code
data = cached_links("https://www.youtube.com/watch?v=PmWQmZXYd74", duplicate_links = false)
(; ids, titles, urls, nodes) = get_nodes(data)
G = grapher(data, ids){111, 110} directed simple Int64 graph
There’s 111 videos in the Graph with 110 connections between the videos.
Here’s what the graph now visualized looks like:
Code
CairoMakie.activate!()
set_theme!(; resolution=(1800, 900))
empty_theme = Theme(
fonts=(; title = "CMU Serif"),
)
# If there's a space it is probably a unique name
node_colors = [length(split(strip(t))) > 1 ? :red : :black for t in titles]
node_size = [length(split(strip(t))) > 1 ? 8 : 5 for t in titles]
f, ax, p = graphplot(G;
nlabels=titles,
nlabels_fontsize=15,
node_color=node_colors,
node_size,
arrow_size=8,
arrow_shift=:end,
layout=Buchheim()
)
hidedecorations!(ax); hidespines!(ax);
offsets = [Point2f(0.1, -1.5) for _ in p[:node_pos][]]
offsets[1] = Point2f(0.1, 0.5)
p.nlabels_offset[] = offsets
autolimits!(ax)
ax.title = "CGP Grey's Rock-Paper-Scissors YouTube Game"
f
There we have it; a flowchart of the Rock-Paper-Scissors game.
If you liked this blog post, consider subscribing to CGP Grey’s Patreon so that they can make more awesome content like this.
If you are interested in viewing all the videos, you can check them out below:
Code
using IJulia
function display_youtube_video(node)
video_id = split(node.url, "=")[end]
title = node.title
html_code = """
<b>$(title)</b>
<br/>
<iframe width="560" height="315" src="https://www.youtube.com/embed/$video_id" frameborder="0" allowfullscreen></iframe>
"""
display("text/html", HTML(html_code))
end
@assert unique(nodes) == nodes
display_youtube_video.(nodes);Reuse
Citation
@online{krishnamurthy2023,
author = {Krishnamurthy, Dheepak},
title = {Graphing {CGP} {Grey’s} {Rock-Paper-Scissors} {YouTube}
{Game}},
pages = {undefined},
date = {2023-10-23},
url = {https://kdheepak.com/blog/graphing-cgpgrey-rock-paper-scissors-youtube-game},
langid = {en}
}